package tools.jackson.databind.node;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import tools.jackson.core.*;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.type.TypeReference;
import tools.jackson.core.type.WritableTypeId;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.testutil.DatabindTestUtil;
import tools.jackson.databind.util.TokenBuffer;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Unit tests for verifying functionality of {@link JsonNode} methods that
 * convert values to other types
 */
public class JsonNodeConversionsTest extends DatabindTestUtil
{
    static class Root {
        public Leaf leaf;
    }

    static class Leaf {
        public int value;

        public Leaf() { }
        public Leaf(int v) { value = v; }
    }

    @JsonDeserialize(using = LeafDeserializer.class)
    public static class LeafMixIn { }

    // Deserializer to trigger the problem described in [JACKSON-554]
    static class LeafDeserializer extends ValueDeserializer<Leaf>
    {
        @Override
        public Leaf deserialize(JsonParser p, DeserializationContext ctxt)
        {
            JsonNode tree = ctxt.readTree(p);
            Leaf leaf = new Leaf();
            leaf.value = tree.get("value").intValue();
            return leaf;
        }
    }

    // for [databind#467]
    @JsonSerialize(using=Issue467Serializer.class)
    static class Issue467Bean  {
        public int i;

        public Issue467Bean(int i0) { i = i0; }
        public Issue467Bean() { this(0); }
    }

    @JsonSerialize(using=Issue467TreeSerializer.class)
    static class Issue467Tree  {
    }

    static class Issue467Serializer extends ValueSerializer<Issue467Bean> {
        @Override
        public void serialize(Issue467Bean value, JsonGenerator g,
                SerializationContext provider) {
            g.writePOJO(new Issue467TmpBean(value.i));
        }
    }

    static class Issue467TreeSerializer extends ValueSerializer<Issue467Tree> {
        @Override
        public void serialize(Issue467Tree value, JsonGenerator g,
                SerializationContext provider) {
            g.writeTree(BooleanNode.TRUE);
        }
    }

    static class Issue467TmpBean  {
        public int x;

        public Issue467TmpBean(int i) { x = i; }
    }

    static class Issue709Bean {
        public byte[] data;
    }

    @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="_class")
    static class LongContainer1940 {
        public Long longObj;
    }

    // [databind#433]
    static class CustomSerializedPojo implements JacksonSerializable
    {
        private final ObjectNode node = JsonNodeFactory.instance.objectNode();

        public void setFoo(final String foo) {
            node.put("foo", foo);
        }

        @Override
        public void serialize(final JsonGenerator jgen, final SerializationContext provider)
        {
            jgen.writeTree(node);
        }

        @Override
        public void serializeWithType(JsonGenerator g,
                SerializationContext ctxt, TypeSerializer typeSer)
        {
            WritableTypeId typeIdDef = new WritableTypeId(this, JsonToken.START_OBJECT);
            typeSer.writeTypePrefix(g, ctxt, typeIdDef);
            serialize(g, ctxt);
            typeSer.writeTypePrefix(g, ctxt, typeIdDef);
        }
    }

    // [databind#4047]
    @JsonRootName("event")
    static class Event {
        public Long id;
        public String name;
    }

    /*
    /**********************************************************************
    /* Test methods
    /**********************************************************************
     */

    private final ObjectMapper MAPPER = objectMapper();

    // note: pre-[databind#5034]
    @Test
    public void testAsBoolean() throws Exception
    {
        assertFalse(BooleanNode.FALSE.asBoolean());
        assertTrue(BooleanNode.TRUE.asBoolean());
        assertFalse(IntNode.valueOf(0).asBoolean());
        assertTrue(IntNode.valueOf(1).asBoolean());
        assertFalse(LongNode.valueOf(0).asBoolean());
        assertTrue(LongNode.valueOf(-34L).asBoolean());
        assertTrue(new StringNode("true").asBoolean());
        assertFalse(new StringNode("false").asBoolean());

        assertTrue(new POJONode(Boolean.TRUE).asBoolean());
    }

    @Test
    public void testTreeToValue() throws Exception
    {
        String JSON = "{\"leaf\":{\"value\":13}}";
        ObjectMapper mapper = jsonMapperBuilder()
                .addMixIn(Leaf.class, LeafMixIn.class)
                .build();
        JsonNode root = mapper.readTree(JSON);
        // Ok, try converting to bean using two mechanisms
        Root r1 = mapper.treeToValue(root, Root.class);
        assertNotNull(r1);
        assertEquals(13, r1.leaf.value);

        // ... also JavaType
        r1 = mapper.treeToValue(root, mapper.constructType(Root.class));
        assertEquals(13, r1.leaf.value);

        // ... also TypeReference
        r1 = mapper.treeToValue(root, new TypeReference<Root>() {});
        assertEquals(13, r1.leaf.value);

        JSON = "[{\"leaf\":{\"value\":13}}, {\"leaf\":{\"value\":12}}]";
        root = mapper.readTree(JSON);
        List<Root> array = mapper.treeToValue(root, new TypeReference<List<Root>>() {});
        assertEquals(2, array.size());
        assertEquals(13, array.get(0).leaf.value);
        assertEquals(12, array.get(1).leaf.value);
    }

    // [databind#1208]: should coerce POJOs at least at root level
    @Test
    public void testTreeToValueWithPOJO() throws Exception
    {
        Calendar c = Calendar.getInstance();
        c.setTime(new java.util.Date(0));
        final ValueNode pojoNode = MAPPER.getNodeFactory().pojoNode(c);
        Calendar result = MAPPER.treeToValue(pojoNode, Calendar.class);
        assertEquals(result.getTimeInMillis(), c.getTimeInMillis());

        // and same with JavaType
        result = MAPPER.treeToValue(pojoNode, MAPPER.constructType(Calendar.class));
        assertEquals(result.getTimeInMillis(), c.getTimeInMillis());
    }

    @Test
    public void testBase64Text() throws Exception
    {
        // let's actually iterate over sets of encoding modes, lengths

        final int[] LENS = { 1, 2, 3, 4, 7, 9, 32, 33, 34, 35 };
        final Base64Variant[] VARIANTS = {
                Base64Variants.MIME,
                Base64Variants.MIME_NO_LINEFEEDS,
                Base64Variants.MODIFIED_FOR_URL,
                Base64Variants.PEM
        };

        for (int len : LENS) {
            byte[] input = new byte[len];
            for (int i = 0; i < input.length; ++i) {
                input[i] = (byte) i;
            }
            for (Base64Variant variant : VARIANTS) {
                StringNode n = new StringNode(variant.encode(input));
                byte[] data = null;
                try {
                    data = n.getBinaryValue(variant);
                } catch (Exception e) {
                    fail("Failed (variant "+variant+", data length "+len+"): "+e.getMessage());
                }
                assertNotNull(data);
                assertArrayEquals(data, input);

                // 15-Aug-2018, tatu: [databind#2096] requires another test
                JsonParser p = new TreeTraversingParser(n);
                assertEquals(JsonToken.VALUE_STRING, p.nextToken());
                try {
                    data = p.getBinaryValue(variant);
                } catch (Exception e) {
                    fail("Failed (variant "+variant+", data length "+len+"): "+e.getMessage());
                }
                assertNotNull(data);
                assertArrayEquals(data, input);
                p.close();
            }
        }
    }

    /**
     * Simple test to verify that byte[] values can be handled properly when
     * converting, as long as there is metadata (from POJO definitions).
     */
    @Test
    public void testIssue709() throws Exception
    {
        byte[] inputData = new byte[] { 1, 2, 3 };
        ObjectNode node = MAPPER.createObjectNode();
        node.put("data", inputData);
        Issue709Bean result = MAPPER.treeToValue(node, Issue709Bean.class);
        String json = MAPPER.writeValueAsString(node);
        Issue709Bean resultFromString = MAPPER.readValue(json, Issue709Bean.class);
        Issue709Bean resultFromConvert = MAPPER.convertValue(node, Issue709Bean.class);

        // all methods should work equally well:
        assertArrayEquals(inputData, resultFromString.data);
        assertArrayEquals(inputData, resultFromConvert.data);
        assertArrayEquals(inputData, result.data);
    }

    @Test
    public void testEmbeddedByteArray() throws Exception
    {
        TokenBuffer buf = TokenBuffer.forGeneration();
        buf.writePOJO(new byte[3]);
        JsonNode node = MAPPER.readTree(buf.asParser(ObjectReadContext.empty()));
        buf.close();
        assertTrue(node.isBinary());
        byte[] data = node.binaryValue();
        assertNotNull(data);
        assertEquals(3, data.length);
    }

    // [databind#232]
    @Test
    public void testBigDecimalAsPlainStringTreeConversion() throws Exception
    {
        ObjectMapper mapper = new ObjectMapper(JsonFactory.builder()
                .enable(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN)
                .build());
        Map<String, Object> map = new HashMap<String, Object>();
        String PI_STR = "3.00000000";
        map.put("pi", new BigDecimal(PI_STR));

        JsonNode tree = mapper.valueToTree(map);
        assertNotNull(tree);
        assertEquals(1, tree.size());
        assertTrue(tree.has("pi"));

        JsonNode tree2 = mapper.writer().valueToTree(map);
        assertEquals(tree, tree2);
    }

    // [databind#433]
    @Test
    public void testBeanToTree() throws Exception
    {
        final CustomSerializedPojo pojo = new CustomSerializedPojo();
        pojo.setFoo("bar");
        final JsonNode node = MAPPER.valueToTree(pojo);
        assertEquals(JsonNodeType.OBJECT, node.getNodeType());

        final JsonNode node2 = MAPPER.writer().valueToTree(pojo);
        assertEquals(JsonNodeType.OBJECT, node2.getNodeType());
        assertEquals(node, node2);
    }

    // [databind#467]
    @Test
    public void testConversionOfPojos() throws Exception
    {
        final Issue467Bean input = new Issue467Bean(13);
        final String EXP = "{\"x\":13}";

        // first, sanity check
        String json = MAPPER.writeValueAsString(input);
        assertEquals(EXP, json);

        // then via conversions: should become JSON Object
        JsonNode tree = MAPPER.valueToTree(input);
        assertTrue(tree.isObject(), "Expected Object, got "+tree.getNodeType());
        assertEquals(EXP, MAPPER.writeValueAsString(tree));

        JsonNode tree2 = MAPPER.writer().valueToTree(input);
        assertEquals(EXP, MAPPER.writeValueAsString(tree2));
    }

    // [databind#467]
    @Test
    public void testConversionOfTrees() throws Exception
    {
        final Issue467Tree input = new Issue467Tree();
        final String EXP = "true";

        // first, sanity check
        String json = MAPPER.writeValueAsString(input);
        assertEquals(EXP, json);

        // then via conversions: should become JSON Object
        JsonNode tree = MAPPER.valueToTree(input);
        assertTrue(tree.isBoolean(),
                "Expected Object, got "+tree.getNodeType());
        assertEquals(EXP, MAPPER.writeValueAsString(tree));

        JsonNode tree2 = MAPPER.writer().valueToTree(tree);
        assertEquals(EXP, MAPPER.writeValueAsString(tree2));
    }

    // [databind#1940]: losing of precision due to coercion
    @Test
    public void testBufferedLongViaCoercion() throws Exception {
        long EXP = 1519348261000L;
        JsonNode tree = MAPPER.readTree("{\"longObj\": "+EXP+".0, \"_class\": \""+LongContainer1940.class.getName()+"\"}");
        LongContainer1940 obj = MAPPER.treeToValue(tree, LongContainer1940.class);
        assertEquals(Long.valueOf(EXP), obj.longObj);
    }

    @Test
    public void testConversionsOfNull() throws Exception
    {
        // First: `null` value should become `NullNode`
        JsonNode n = MAPPER.valueToTree(null);
        assertNotNull(n);
        assertTrue(n.isNull());
        n = MAPPER.writer().valueToTree(null);
        assertNotNull(n);
        assertTrue(n.isNull());

        // and vice versa
        Object pojo = MAPPER.treeToValue(n, Root.class);
        assertNull(pojo);

        pojo = MAPPER.treeToValue(n, MAPPER.constructType(Root.class));
        assertNull(pojo);

        // [databind#2972]
        AtomicReference<?> result = MAPPER.treeToValue(NullNode.instance,
                AtomicReference.class);
        assertNotNull(result);
        assertNull(result.get());

        result = MAPPER.treeToValue(NullNode.instance,
                MAPPER.constructType(AtomicReference.class));
        assertNotNull(result);
        assertNull(result.get());
    }

    // Simple cast, for Tree
    @Test
    public void testNodeConvert() throws Exception
    {
        ObjectNode src = (ObjectNode) MAPPER.readTree("{}");
        TreeNode node = src;
        ObjectNode result = MAPPER.treeToValue(node, ObjectNode.class);
        // should just cast...
        assertSame(src, result);

        // ditto w/ JavaType
        result = MAPPER.treeToValue(node, MAPPER.constructType(ObjectNode.class));
        assertSame(src, result);
    }

    // [databind#4047] : ObjectMapper.valueToTree will ignore the configuration SerializationFeature.WRAP_ROOT_VALUE
    @Test
    public void testValueToTree() throws Exception
    {
        // Arrange
        Event value = new Event();
        value.id = 1L;
        value.name = "foo";

        ObjectMapper wrapRootMapper = jsonMapperBuilder()
                .enable(SerializationFeature.WRAP_ROOT_VALUE)
                .build();

        // Act & Assert
        String expected = "{\"event\":{\"id\":1,\"name\":\"foo\"}}";
        assertEquals(wrapRootMapper.readValue(expected, Map.class), wrapRootMapper.readValue(wrapRootMapper.writeValueAsString(value), Map.class));
        assertEquals(wrapRootMapper.readValue(expected, Map.class), wrapRootMapper.readValue(wrapRootMapper.valueToTree(value).toString(), Map.class));
    }

    // [databind#4932]: handling of `MissingNode` wrt conversions
    @Test
    public void treeToValueWithMissingNode4932() throws Exception {
        assertNull(MAPPER.treeToValue(MAPPER.nullNode(), Object.class));
        assertNull(MAPPER.treeToValue(MAPPER.missingNode(), Object.class));

        ObjectReader r = MAPPER.readerFor(Object.class);
        assertNull(r.treeToValue(MAPPER.nullNode(), Object.class));
        assertNull(r.treeToValue(MAPPER.missingNode(), Object.class));
    }
}
